<template>
  <div>
    <input type="file" name="file" @change="handleFileChange" />
    <el-button @click="uploadFile">上传</el-button>
  </div>
</template>

<script>
import sparkMD5 from "spark-md5";
const CHUNK_SIZE = 10 * 1024 * 1024;

export default {
  data() {
    return {
      file: null,
      // uploadProgress:0,
      hashProgress: 0,
      chunks: [],
    };
  },
  methods: {
    async uploadFile() {
      //
      if (!this.file) {
        return;
      }
      // 文件分割
      const chunks = this.createFileChunk();
      this.chunks = chunks;
      // 计算文件 hash 值，确定文件唯一
      const hash = await this.calculateHashIdle();
      this.hash = hash;

      // 问一下后端，文件是否上传过，如果没有，是否有存在的切片
      const {
        data: { uploaded, uploadedList },
      } = await this.$http.post("/checkfile", {
        hash: this.hash,
        ext: this.file.name.split(".").pop(),
      });
      if (uploaded) {
        // 秒传
        return this.$message.success("秒传成功");
      } else {
        this.chunks = chunks.map((chunk, index) => {
          // 切片的名字 hash+index
          const name = hash + "-" + index;
          return {
            hash,
            name,
            index,
            chunk: chunk.file,
            // 设置进度条，已经上传的，设为100
            progress: uploadedList.indexOf(name) > -1 ? 100 : 0,
          };
        });

        await this.uploadChunks(uploadedList);
      }
    },

    async uploadChunks(uploadedList = []) {
      const requests = this.chunks
        .filter((chunk) => uploadedList.indexOf(chunk.name) === -1)
        .map((chunk) => {
          // 转成promise
          const form = new FormData();
          form.append("chunk", chunk);
          form.append("hash", chunk.hash);
          form.append("name", chunk.name);
          return { form, index: chunk.index, error: 0 };
        })
        .map(({ form, index }) => {
          // 异步并发 请求限制
          this.$http.post("/uploadfile", form, {
            onUploadProgress: (progress) => {
              // 不是整体的进度条了，而是每个区块有自己的进度条，整体的进度条需要计算
              this.chunks[index].progress = Number(
                ((progress.loaded / progress.total) * 100).toFixed(2)
              );
            },
          });
        });

      await Promise.all(requests);
      // 合并文件 请求
      await this.mergeRequest();
    },

    // 60fps
    // 1秒渲染60次 渲染1次 1帧，大概16.6ms
    // |帧(system task，render，script)空闲时间  |帧 painting idle   |帧   |帧   |
    // 借鉴fiber架构
    calculateHashIdle() {
      const chunks = this.chunks;

      return new Promise((resolve) => {
        const spark = new sparkMD5.ArrayBuffer();
        let count = 0;

        const appendToSpark = async (file) => {
          return new Promise((resolve) => {
            const reader = new FileReader();
            reader.readAsArrayBuffer(file);
            reader.onload = (e) => {
              spark.append(e.target.result);
              resolve();
            };
          });
        };

        const workLoop = async (deadline) => {
          // timeRemaining获取当前帧的剩余时间
          if (count < chunks.length && deadline.timeRemaining() > 1) {
            // 空闲时间，且有任务
            await appendToSpark(chunks[count].file);
            count++;
            // 计算 hash 进度
            if (count < chunks.length) {
              this.hashProgress = Number(
                ((100 * count) / chunks.length).toFixed(2)
              );
            } else {
              this.hashProgress = 100;
              resolve(spark.end());
            }
            window.requestIdleCallback(workLoop);
          }
        };

        // 浏览器一旦空闲，就会调用workLoop
        window.requestIdleCallback(workLoop);
      });
    },
    createFileChunk(size = CHUNK_SIZE) {
      const chunks = [];
      let cur = 0;
      while (cur < this.file.size) {
        chunks.push({ index: cur, file: this.file.slice(cur, cur + size) });
        cur += size;
      }
      return chunks;
    },

    async mergeRequest() {
      const ret = await this.$http.post("/mergefile", {
        ext: this.file.name.split(".").pop(),
        size: CHUNK_SIZE,
        hash: this.hash,
      });
      // 返回文件地址
      const url = ret.data.url;
      await this.$http.put("/user/info", { url: "/api" + url });
    },

    handleFileChange(e) {
      console.log(e);
      const [file] = e.target.files;
      if (!file) {
        return;
      }
      this.file = file;
    },
  },
};
</script>
